<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Vuetify Components::Thymeleaf</title>
</head>
<body>
  <!-- ######################## -->
  <!--/* 带Confirm提示的按钮组件 */-->
  <!-- ######################## -->
  <th:block th:fragment="v-btn-confirm">
    <template id="v-btn-confirm-template">
      <v-menu top offset-y :close-on-content-click="false" :disabled="disabled" v-model="visible">
        <template v-slot:activator="{ on, attrs }">
          <a v-bind="attrs" v-on="on" :class="fclass" ref="button">
            <slot>{{$t('删除')}}</slot>
          </a>
        </template>
        <v-list style="padding: 0;">
          <v-list-item>
            <v-list-item-content style="display: block;">
              <v-list-item-title style="font-size: 14px;">
                <v-icon color="warning">{{icon}}</v-icon><span class="confrim-message" v-html="$t(message)"></span>
              </v-list-item-title>
              <v-list-item-subtitle class="text-center">
                <v-btn small depressed outlined text class="mr-2" :disabled="loading" @click="cancel">{{$t(cancelText)}}</v-btn>
                <v-btn small depressed color="primary" :loading="loading" @click="confirm">{{$t(confirmText)}}</v-btn>
              </v-list-item-subtitle>
            </v-list-item-content>
          </v-list-item>
        </v-list>
      </v-menu>
    </template>
    <script type="text/javascript">
      Vue.component('v-btn-confirm', {
        template: '#v-btn-confirm-template',
        data: function() {
          return {
            visible: false,
            loading: false,
            manual: false
          }
        },
        computed: {
          fclass() {
            return ((this.clazz??'') + (this.visible? ' op-deleting':'')) || undefined;
          }
        },
        watch: {
          visible(v) {
            let eventType = null;
            if (v) {
              this.manual = this.loading? this.manual : false;
              eventType = "open";
            } else {
              if (!this.manual && !this.loading) {
                eventType = "close";
              }
            }
            if (eventType) this.$emit(v?"open":"close", this.data, this);
            this.toggleHighlight(v);
          }
        },
        props: {
          message: {
            type: String,
            default: '确定删除？'
          },
          icon: {
            type: String,
            default: 'mdi-message-question'
          },
          confirmText: {
            type: String,
            default: '确定'
          },
          cancelText: {
            type: String,
            default: '取消'
          },
          disabled: {
            type: Boolean,
            default: false
          },
          clazz: {
            type: String
          },
          data: Object
        },
        methods: {
          confirm() {
            this.loading = true;
            this.manual = true;
            this.$emit("confirm", this.data, this);
            window.confirmObj = this;
          },
          cancel() {
            this.manual = true;
            this.close();
            this.$emit("cancel", this.data, this);
          },
          close() {
            this.visible = false;
          },
          finish() {
            this.close();
            this.loading = false;
          },
          toggleHighlight(v) {
            let hclazz = 'v-highlight';
            let parentNode = this.$refs.button.parentNode.parentNode;
            if (parentNode) {
              if (parentNode.classList.contains('v-data-table__expanded__content')) {
                let sibling = parentNode.previousElementSibling;
                if (sibling && sibling.classList.contains('v-data-table__expanded__row')) {
                  sibling.classList.toggle(hclazz)
                }
              } else {
                if (parentNode.classList.contains('row')) { //TreeGrid
                  while (parentNode) {
                    if (parentNode.classList.contains('v-treeview-node__root')) {
                      this.$nextTick(() => {
                        v? parentNode.classList.add(hclazz) : parentNode.classList.remove(hclazz);
                      })
                      break;
                    }
                    parentNode = parentNode.parentNode;
                  }
                } else { //Datatable
                  parentNode.classList.toggle(hclazz);
                }
              }
            }
          }
        }
      });
    </script>
  </th:block>

  <!-- ################# -->
  <!--/* 自定义下拉树组件 */-->
  <!-- ################# -->
  <th:block th:fragment="v-tree-select">
    <template id="v-tree-select-template">
      <v-menu offset-y :close-on-content-click="!multiple" :dark="dark" v-model="innerMenu">
        <template v-slot:activator="{ on, attrs }">
          <v-text-field v-bind="attrs" v-on="on" outlined dense hide-details readonly :dark="dark" :clearable="clearable" :disabled="disabled" :loading="loading" :class="fullClass" :label="label" :error-messages="errorMessages" v-model="innerText" @click:clear="clear"/>
        </template>

        <v-list class="mt-1 overflow-y-auto" v-show="!readonly" max-height="300">
          <v-list-item class="pa-0">
            <v-list-item-content class="pa-0">
              <v-treeview ref="treeViewRef" class="tree-select" dense hoverable activatable return-object :item-key="itemKey" :item-text="itemText" :items="items"
                          @update:active="nodesActive" :open="open" :active="active" :multiple-active="multiple"/>
            </v-list-item-content>
          </v-list-item>
        </v-list>
      </v-menu>
    </template>
    <script type="text/javascript">
      Vue.component('v-tree-select', {
        template: "#v-tree-select-template",
        data: function() {
          return {
            innerText: "",
            innerMenu: false
          }
        },
        watch: {
          innerMenu(val) {
            if (val) {
              if (this.active && this.active.length == 0 && this.$refs.treeViewRef) {
                this.$refs.treeViewRef.updateAll(false);
              }
            }
          },
          items() {
            this.changeInnerText(this.active);
            this.checkReadonly();
          },
          active(val, oldVal) {
            let emit = true;
            if (this.mandatory) {
              emit = val.length > 0;
            }

            if (emit) {
              this.$emit("input", val);
              this.changeInnerText(val);
            }

            if (this.items.length > 0 && this.mandatory && val.length === 0 && oldVal.length > 0) {
              this.active = [...oldVal];
            }
          }
        },
        model: {
          prop: 'active',
          event: 'input'
        },
        props: {
          dark: {type: Boolean, default: false},
          loading: {type: Boolean, default: false},
          mandatory: {type: Boolean, default: false},
          multiple: {type: Boolean, default: false},
          readonly: {type: Boolean, default: false},
          readonlyAuto: {type: Boolean, default: false},
          disabled: {type: Boolean, default: false},
          clearable: {type: Boolean, default: true},
          clazz: {type: String, default: ""},
          items: {type: Array, default: []},
          itemKey: {type: String, default: "id"},
          itemText: {type: String, default: "name"},
          label: {type: String, default: ""},
          errorMessages: {type: Array, default: []},
          open: {type: Array, default: []},
          active: {type: Array, default: []}
        },
        computed: {
          fullClass() {
            return this.clazz + (this.readonly?' cursor-not-allowed':'') + ((this.$vuetify.theme.dark || this.dark)? '' : ' white');
          }
        },
        methods: {
          nodesActive(arr) {
            this.active = arr;
          },
          changeInnerText(arr) {
            this.$nextTick(() => {
              if (arr.length > 0) {
                let texts = "";
                for (let node of arr) {
                  let v = node[this.itemText];
                  if (v) {
                    texts += (v + ", ");
                  }
                }
                texts = texts.trim();
                this.innerText = texts.length > 0? texts.substr(0, texts.length - 1) : texts;
              } else {
                this.innerText = "";
              }
            });
          },
          checkReadonly() {
            if (this.readonlyAuto) { // 启动了只读自动识别
              let _readonly = false;
              if (this.items && this.items.length <= 1) {
                _readonly = true;
                if (this.items.length > 0) {
                  let children = this.items[0].children;
                  _readonly = !(children && children.length > 0);
                }
              }
              this.readonly = _readonly;
            }
          },
          clear() {
            this.active = [];
          }
        },
        mounted() {
          this.changeInnerText(this.active);
          this.checkReadonly();
        }
      });
    </script>
  </th:block>

  <!-- ######################### -->
  <!--/* 自定义全屏Loading提示插件 */-->
  <!-- ######################### -->
  <th:block th:fragment="v-loading">
    <template id="v-loading-template">
      <v-overlay :value="value">
        <v-progress-circular indeterminate :size="size"></v-progress-circular>
      </v-overlay>
    </template>
    <script type="text/javascript">
      Vue.component('v-loading', {
        template: '#v-loading-template',
        props: {
          value: {
            type: Boolean,
            default: false
          },
          size: {
            type: Number,
            default: 64
          }
        }
      });
    </script>
  </th:block>

  <!-- ###################### -->
  <!--/* 支持全屏展开的Card组件 */-->
  <!-- ###################### -->
  <th:block th:fragment="v-card-zoom">
    <template id="v-card-zoom-template">
      <v-card :class="{'v-card--max':isMax}" @mouseover="toolbar=true" @mouseout="toolbar=false" :color="color" :flat="flat" :dark="dark" :light="light" :disabled="disabled" :rounded="rounded" :shaped="shaped" :loading="loading" :elevation="elevation" :img="img" :width="width" :height="height" :max-width="maxWidth" :min-width="minWidth" :max-height="maxHeight" :min-height="minHeight" :tile="tile">
        <div style="position:absolute; right:0; top:0; z-index:9">
          <v-btn icon x-small v-if="enableRefresh" v-show="toolbar" :color="iconColor" @click="refreshHandle">
            <v-icon>mdi-refresh</v-icon>
          </v-btn>
          <v-btn icon x-small v-show="toolbar" :color="iconColor" @click="zoom">
            <v-icon>{{iconZoom}}</v-icon>
          </v-btn>
        </div>
        <slot></slot>
      </v-card>
    </template>
    <script type="text/javascript">
      Vue.component('v-card-zoom', {
        template: '#v-card-zoom-template',
        data() {
          return {
            iconZoom: 'mdi-arrow-expand',
            toolbar: false
          }
        },
        computed: {
          refreshHandle() {
            return this._events.refresh || null;
          },
          enableRefresh() {
            return this.refreshHandle? 1 : 0;
          }
        },
        model: {
          prop: 'isMax',
          event: 'zoom'
        },
        props:{
          isMax: {type:Boolean, default:false},
          iconColor:String,
          flat:Boolean,
          light:Boolean,
          dark:Boolean,
          disabled:Boolean,
          shaped:Boolean,
          tile:Boolean,
          color:String,
          img:String,
          loading:{type:Boolean|String,default:false},
          rounded:{type:Boolean|String,default:false},
          elevation:{type:Number|String,default:undefined},
          width:{type:Number|String,default:undefined},
          maxWidth:{type:Number|String,default:undefined},
          minWidth:{type:Number|String,default:undefined},
          height:{type:Number|String,default:undefined},
          maxHeight:{type:Number|String,default:undefined},
          minHeight:{type:Number|String,default:undefined}
        },
        methods: {
          zoom() {
            this.isMax = !this.isMax;
            this.iconZoom = this.isMax? "mdi-arrow-collapse" : "mdi-arrow-expand";
            this.$nextTick(() => {
              this.$emit("zoom", this.isMax);
            });
          }
        }
      });
    </script>
  </th:block>

  <!-- ################# -->
  <!--/* 下拉日期控件 */-->
  <!-- ################# -->
  <th:block th:fragment="v-date-picker-select">
    <template id="v-date-picker-select-template">
      <v-menu v-model="pickerMenu" :dark="dark" :close-on-content-click="false" :nudge-right="nudgeright" :nudge-top="-2" transition="scale-transition" offset-y min-width="auto">
        <template v-slot:activator="{ on, attrs }">
          <v-text-field v-show="vshow" :id="id" v-model="picker" :label="label" :prepend-icon="icon" hide-details readonly dense outlined :clearable="clearable" v-bind="attrs" v-on="on"></v-text-field>
        </template>
        <v-date-picker v-model="picker" :min="min" :max="max" :width="width" no-title scrollable @input="pickerMenu=false" :day-format="pickerDayFormat" :type="type">
          <v-spacer></v-spacer>
          <v-btn text color="primary" @click="changeDate(-1)" v-if="showYesterday && type=='date'">昨天</v-btn>
          <v-btn text color="primary" @click="changeDate(0)" v-if="showToday && type=='date'">今天</v-btn>
          <v-btn text color="primary" @click="changeMonth(-1)" v-if="type=='month'">上月</v-btn>
          <v-btn text color="primary" @click="changeMonth(0)" v-if="type=='month'">本月</v-btn>
        </v-date-picker>
      </v-menu>
    </template>
    <script type="text/javascript">
      Vue.component('v-date-picker-select', {
        template: '#v-date-picker-select-template',
        data: function() {
          return {
            pickerMenu: false,
            formatDay: 'yyyy-MM-dd',
            formatMonth: 'yyyy-MM'
          }
        },
        watch: {
          picker(val) {
            this.$emit("input", val);
          },
          pickerMenu(val) {
            this.$emit(val? 'show' : 'hide');
          }
        },
        model: {
          prop: 'picker',
        },
        props: {
          id: {type: String},
          picker: {type: String},
          label: {type: String, default:'日期'},
          icon: {type: String, default:'mdi-calendar'},
          dark: {type: Boolean, default: false},
          clearable: {type: Boolean, default: false},
          min: {type: String},
          max: {type: String},
          nudgeright: {type: Number, default:40},
          width: {type:Number},
          type: {type: String, default:'date'},
          vshow: {type: Boolean, default: true}
        },
        computed: {
          showYesterday() {
            return this.coverRange(this.getDate(-1))
          },
          showToday() {
            return this.coverRange(this.getDate(0))
          }
        },
        methods: {
          pickerDayFormat(val) {
            return val.substr(-2);
          },
          getDate(offsetDay) {
            let date = new Date();
            date.setDate(date.getDate() + offsetDay);
            date.setHours(0,0,0,0);
            return date;
          },
          getMonthDate(offsetMonth) {
            let date = new Date();
            date.setMonth(date.getMonth() + offsetMonth);
            date.setHours(0,0,0,0);
            return date;
          },
          coverRange(date) {
            let minDate = typeof(this.min) === 'string'? Date.of(this.min) : new Date(0);
            let maxDate = new Date();
            maxDate.setFullYear(9999);
            maxDate = typeof(this.max) === 'string'? Date.of(this.max) : maxDate;

            let time = date.getTime();
            return time >= minDate.getTime() && time <= maxDate.getTime();
          },
          changeDate(offsetDay) {
            this.picker = this.getDate(offsetDay).format(this.formatDay);
            this.pickerMenu = false;
          },
          changeMonth(offsetMonth) {
            this.picker = this.getMonthDate(offsetMonth).format(this.formatMonth);
            this.pickerMenu = false;
          },
        }
      });
    </script>
  </th:block>
</body>
</html>